
--[[
*********************************************************************************
** 
** Copyright 2021-2024 LuaOS
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
** SOFTWARE.
** 
*********************************************************************************
]]--

--------------------------------------------------------------------------------

local format     = string.format;
local split      = string.split;
local match      = string.match;
local insert     = table.insert;
local concat     = table.concat;
local unescape   = io.http.unescape;

--------------------------------------------------------------------------------

local http_status_text = {
  [100] = "Continue",
  [101] = "Switching Protocols",
  [200] = "OK",
  [201] = "Created",
  [202] = "Accepted",
  [203] = "Non-Authoritative Information",
  [204] = "No Content",
  [205] = "Reset Content",
  [206] = "Partial Content",
  [300] = "Multiple Choices",
  [301] = "Moved Permanently",
  [302] = "Found",
  [303] = "See Other",
  [304] = "Not Modified",
  [305] = "Use Proxy",
  [307] = "Temporary Redirect",
  [400] = "Bad Request",
  [401] = "Unauthorized",
  [402] = "Payment Required",
  [403] = "Forbidden",
  [404] = "File Not Found",
  [405] = "Method Not Allowed",
  [406] = "Not Acceptable",
  [407] = "Proxy Authentication Required",
  [408] = "Request Time-out",
  [409] = "Conflict",
  [410] = "Gone",
  [411] = "Length Required",
  [412] = "Precondition Failed",
  [413] = "Request Entity Too Large",
  [414] = "Request-URI Too Large",
  [415] = "Unsupported Media Type",
  [416] = "Requested range not satisfiable",
  [417] = "Expectation Failed",
  [500] = "Internal Server Error",
  [501] = "Not Implemented",
  [502] = "Bad Gateway",
  [503] = "Service Unavailable",
  [504] = "Gateway Time-out",
  [505] = "HTTP Version not supported",
};

--------------------------------------------------------------------------------

local http_mime_type = {
  cod   = "image/cis-cod",
  ras   = "image/cmu-raster",
  fif   = "image/fif",
  gif   = "image/gif",
  ief   = "image/ief",
  jpeg  = "image/jpeg",
  jpg   = "image/jpeg",
  jpe   = "image/jpeg",
  png   = "image/png",
  tiff  = "image/tiff",
  tif   = "image/tiff",
  mcf   = "image/vasa",
  wbmp  = "image/vnd.wap.wbmp",
  fh4   = "image/x-freehand",
  fh5   = "image/x-freehand",
  fhc   = "image/x-freehand",
  pnm   = "image/x-portable-anymap",
  pbm   = "image/x-portable-bitmap",
  pgm   = "image/x-portable-graymap",
  ppm   = "image/x-portable-pixmap",
  rgb   = "image/x-rgb",
  ico   = "image/x-icon",
  xwd   = "image/x-windowdump",
  xbm   = "image/x-xbitmap",
  xpm   = "image/x-xpixmap",
  js    = "text/javascript",
  css   = "text/css",
  htm   = "text/html",
  html  = "text/html",
  shtml = "text/html",
  txt   = "text/plain",
  pdf   = "application/pdf",
  xml   = "application/xml",
  json  = "application/json",
};

local rpcall = rpc.new(0, 0);
local os_version = "luaos" .. "/" .. os.version();
local http_filter = {pattern={}, method={}};

--------------------------------------------------------------------------------

local function luaos_version(request)
  return os_version;
end

--------------------------------------------------------------------------------

local function request_error(request)
  return http_status_text[403];
end

--------------------------------------------------------------------------------

local functions = {
  ["http:index"] = luaos_version,
  ["http:error"] = request_error,
};

--------------------------------------------------------------------------------

local function http_response(peer, code, header, body, encoding)
  body = body or "";
  local headers = {
    ["Server"]         = luaos_version(),
    ["Cache-Control"]  = "max-age=0",
    ["Connection"]     = "Close",
    ["Content-Length"] = format("%d", #body),
    ["Content-Type"]   = http_mime_type.html .. ";charset=UTF-8",
  };
  if header then
    for k, v in pairs(header) do
      if not headers[k] then
        headers[k] = v;
      end
    end
  end

  if encoding then
    headers["Content-Encoding"] = encoding;
  end

  local response = {};
  local status   = format("HTTP/1.1 %d %s\r\n", code, http_status_text[code]);
  insert(response, status);
  for k, v in pairs(headers) do
    insert(response, k);
    insert(response, ": ");
    insert(response, v);
    insert(response, "\r\n");
  end
  insert(response, "\r\n");
  insert(response, body);
  peer:send(concat(response));
end

--------------------------------------------------------------------------------

local function check_access(method)
  local ms = http_filter.method;
  if type(ms) == "table" then
    for k, v in ipairs(ms) do
      if v == method then
        return true;
      end
    end
  end
  local pt = http_filter.pattern;
  if type(pt) == "table" then
    for k, v in ipairs(pt) do
      if match(method, v) then
        return true;
      end
    end
  end
  return false;
end

--------------------------------------------------------------------------------

local function parse_method(context)
  local method = {};
  local paths  = context.path:split("/");
  for _, v in ipairs(paths) do
    if #v > 0 then
      if #method > 0 then
        insert(method, ":");
      end
      insert(method, v);
    end
  end
  method = concat(method);
  if not method or #method == 0 then
    method = "http:index";
  elseif not check_access(method) then
    method = "http:error";
  end
  return method;
end

--------------------------------------------------------------------------------

local function parse_query(context)
  local query = context.query;
  if query and #query > 0 then
    local t = split(query, "&");
    query = {};
    for k, v in pairs(t) do
      local s = split(v, "=");
      query[s[1]] = unescape(s[2]);
    end
  else
    query = nil;
  end
  return query;
end

--------------------------------------------------------------------------------

local function co_on_request(session)
  local headers = session.headers;
  local url     = session.url;
  local peer    = session.socket;
  local body    = session.body;
  local context = io.http.parse_url(url);
  local query   = parse_query(context);
  local method  = parse_method(context);
  
  local status  = 200;
  local request = {
    query   = query,
    from    = peer:endpoint();
    body    = body,
    headers = headers,
  };
  local ok, result, response = rpcall(method, request);
  if not ok then
    status = 500;
    if not os.debugging() then
      result = http_status_text[500];
    else      
      result = result:gsub("\n", "<br>");
    end
  end
  result = tostring(result or "");

  local encoding = nil;
  if #result > 0 then
    encoding = headers["Accept-Encoding"];
    if encoding then
      if encoding:find("gzip") then
        encoding = "gzip";
        result = compress(result, encoding);
      else
        encoding = nil;
      end
    end
  end
  http_response(peer, status, response, result, encoding);
end

--------------------------------------------------------------------------------

local function rp_on_request(session)
  local parser = session.parser;
  local peer   = session.socket;
  local co     = session.co;
  local method = parser:method();

  if method ~= "GET" and method ~= "POST" then
    http_response(peer, 405, nil, "Method Not Allowed");
    return;
  end
  co:dispatch(bind(co_on_request, session));
end

--------------------------------------------------------------------------------

local function http_on_request(session)
  if session.body then
    session.body = concat(session.body);
    if session.body == "" then
      session.body = nil;
    end
  end
  local ok, err = pcall(rp_on_request, session);
  if not ok then
    error(err);
    local peer = session.socket;
    peer:close();
  end
end

--------------------------------------------------------------------------------

local function http_on_error(session)
  session.co = nil;
  session.socket:close();
  session.socket = nil;
end

--------------------------------------------------------------------------------

local function http_on_receive(session, data, ec)
  local peer = session.socket;
  if ec then
    session.co:close(bind(http_on_error, session));
    return;
  end
  local parser = session.parser;
  while #data > 0 do
    local parsed = parser:execute(data);
    if parser:error() > 0 then
      peer:close();
      return;
    end
    if parsed == #data then
      break;
    end
    data = data:sub(parsed + 1);
  end
end

--------------------------------------------------------------------------------

local function http_on_url(session, url)
  session.url = url;
end

--------------------------------------------------------------------------------

local function http_on_body(session, data)
  if not session.body then
    session.body = {};
  end
  insert(session.body, data);
end

--------------------------------------------------------------------------------

local function http_on_begin(session)
  session.url  = "";
  session.body = nil;
  session.headers = {};
end

--------------------------------------------------------------------------------

local function http_on_header(session, name, value)
  session.headers[name] = value;
end

--------------------------------------------------------------------------------

local function http_new_session(peer)
  local session = {
    socket  = peer,
    url     = "",
    parser  = nil,
    body    = nil,
    headers = {},
	co = os.coroutine();
  };
  local options = {
    on_message_begin    = bind(http_on_begin,   session),
    on_body             = bind(http_on_body,    session),
    on_header           = bind(http_on_header,  session),
    on_url              = bind(http_on_url,     session),
    on_message_complete = bind(http_on_request, session)
  };
  session.parser = io.http.request_parser(options);
  return session;
end

--------------------------------------------------------------------------------

local function http_on_accept(peer, ec)
  if ec then
    peer:close();
    return;
  end
  local session = http_new_session(peer);
  peer:receive(bind(http_on_receive, session));
end

--------------------------------------------------------------------------------

local function init_context(pemfile, keyfile, password)
  local context  = nil;
  if pemfile and keyfile then
    context = io.context();
    context:load(pemfile, keyfile, password);
    local exp = context:period();
    if exp then
      local t1 = exp[1];
      local t2 = exp[2];
      print(string.format("certificate validity period: %02d-%02d-%02d to %02d-%02d-%02d", t1.year, t1.month, t1.day, t2.year, t2.month, t2.day));
    end
  end
  return context;
end

--------------------------------------------------------------------------------

local function main(filter, port, host, pemfile, keyfile, password)
  port = port or (pemfile and 443 or 80);
  local context = init_context(pemfile, keyfile, password);
  local server  = io.server("tcp", context);
  local ok = server:listen(host or "0.0.0.0", port, http_on_accept);
  if not ok then
    error(format("socket listen error at port: %d", port));
    return;
  end
  http_filter = filter or {};
  if not http_filter.method then
    http_filter.method = {};
  end
  for k, v in pairs(functions) do
    insert(http_filter.method, k);
    rpc.create(k, v);
  end
  print(format("%s works on port %d", os.name(), port));
  while not os.stopped() do
    os.wait();
  end
  server:close();
end

--------------------------------------------------------------------------------

luaos.start(main, ...);

--------------------------------------------------------------------------------
